/*
 * Calendar.java
 *
 * Created on November 9, 2006, 3:11 PM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package edu.umn.cs5115.scheduler.calendar;

import edu.umn.cs5115.scheduler.SchedulerDocument;
import edu.umn.cs5115.scheduler.entities.Course;
import edu.umn.cs5115.scheduler.entities.CoursePart;
import edu.umn.cs5115.scheduler.entities.MeetingTime;
import edu.umn.cs5115.scheduler.entities.Schedule;
import edu.umn.cs5115.scheduler.entities.Section;
import edu.umn.cs5115.scheduler.entities.Weekday;
import edu.umn.cs5115.scheduler.framework.DocumentController;
import edu.umn.cs5115.scheduler.framework.KeyValueAdapter;
import edu.umn.cs5115.scheduler.framework.KeyValueCoding;
import edu.umn.cs5115.scheduler.framework.KeyValueObserver;
import edu.umn.cs5115.scheduler.framework.KeyValueObserver.SetMutationType;
import edu.umn.cs5115.scheduler.framework.ListenerManager;
import edu.umn.cs5115.scheduler.framework.MenuAvailabilityListener;
import edu.umn.cs5115.scheduler.framework.gui.AlphaColor;
import edu.umn.cs5115.scheduler.framework.gui.Binder;
import edu.umn.cs5115.scheduler.framework.gui.FakeCanvas;
import edu.umn.cs5115.scheduler.framework.gui.FakeWidget;
import edu.umn.cs5115.scheduler.framework.gui.Font;
import edu.umn.cs5115.scheduler.framework.gui.KeyValueCodingCheckbox;
import edu.umn.cs5115.scheduler.framework.gui.SelectionController;
import edu.umn.cs5115.scheduler.framework.gui.ValueTransformer;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.FontMetrics;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.ScrollBar;

/**
 * Puts all the pieces of the calendar together.
 * @author grant
 */
public class Calendar extends Composite implements PaintListener
{
    private ScrollableCalendar calendarScroller;
    /**
     * Creates a new instance of Calendar
     */
    public Calendar(Composite parent, SchedulerDocument document)
    {
        super(parent, SWT.NO_BACKGROUND);
        calendarScroller = new ScrollableCalendar(this, document);
        addControlListener(new ControlAdapter()
        {
            public void controlResized(ControlEvent e)
            {
                redoLayout();
            }
        });
        addPaintListener(this);
    }
    
    public void paintControl(PaintEvent e)
    {
        calendarScroller.getCalendar().paintDayLabels(e);
    }
    
    /**
     *
     * Calendar does its layout manually.
     */
    private void redoLayout()
    {
        Rectangle area = getClientArea();
        int labelHeight = calendarScroller.getCalendar().getDayLabelHeight();
        calendarScroller.setBounds(0, labelHeight,area.width, area.height-labelHeight);
    }
    
    /**
     * Makes the calendar scrollable.
     * @author grant
     */
    private static class ScrollableCalendar extends Composite
    {
        private CalendarInternal myCalendar;
        private double calendarZoomFactor = 1.8;
        private ScrollBar scrollBar;
        
        /** Creates a new instance of ScrollableCalendar */
        public ScrollableCalendar(Composite parent, SchedulerDocument document)
        {
            super(parent, SWT.NO_BACKGROUND | SWT.V_SCROLL);
            myCalendar = new CalendarInternal(this, document);
            scrollBar = getVerticalBar();
            scrollBar.setIncrement(5);
            scrollBar.addListener(SWT.Selection, new Listener()
            {
                public void handleEvent(Event e)
                {
                    repositionCalendar();
                }
            });
            addControlListener(new ControlAdapter()
            {
                public void controlResized(ControlEvent e)
                {
                    Rectangle myArea = getClientArea();
                    Point oldSize = myCalendar.getSize();
                    Point newSize = new Point(myArea.width, (int)(myArea.height * calendarZoomFactor));
                    int newSelection = 0;
                    if (oldSize.y > 0)
                        newSelection = (int)(((double)scrollBar.getSelection()/oldSize.y)*newSize.y);
                    myCalendar.setSize(newSize);
                    scrollBar.setMaximum(newSize.y);
                    scrollBar.setThumb(myArea.height);
                    scrollBar.setPageIncrement((int)(myArea.height*.9)+1);
                    scrollBar.setSelection(newSelection);
                    repositionCalendar();
                }
            });
            
            // scroll to make about 8AM at the top of the calendar
            myCalendar.setSize(100,1000); // we just need a non-zero size.
            scrollBar.setMaximum(1000);
            scrollBar.setSelection(327);
            repositionCalendar();
        }
        
        private void repositionCalendar()
        {
            Point location = myCalendar.getLocation();
            location.y = -scrollBar.getSelection();
            myCalendar.setLocation(location);
        }
        
        /** Get the calendar control being scrolled. */
        public CalendarInternal getCalendar()
        {
            return myCalendar;
        }
    }
    
    /**
     * This class displays a bunch of Events on a calendar.  If you want it pre-
     * packaged into a scroller, see the ScrollableCalendar class.
     * @see ScrollableCalendar
     * @author grant
     */
    private static class CalendarInternal extends FakeCanvas implements PaintListener
    {
        /** Main calendar's background color. */
        private final static RGB CALENDAR_BACKGROUND_COLOR = new RGB(255, 255, 255);
        /** Background color for the night time. (6pm-6am) */
        private final static RGB NIGHT_COLOR = new RGB(239, 239, 244);
        /** vertical lines separating the days of the week. */
        private final static RGB DAY_SEPARATORS_COLOR = new RGB(233, 233, 233);
        /** Horizontal lines separating every hour. */
        private final static RGB HOUR_SEPARATORS_COLOR = DAY_SEPARATORS_COLOR;
        /** Horizontal lines separating every half hour. */
        private final static RGB HALF_HOUR_SEPARATORS = new RGB(191, 191, 191);
        /** Text color to use for the week days. */
        private final static RGB DAY_TEXT_COLOR = new RGB(0, 0, 0);
        /** Font Data used to create the font used to draw the day labels. */
        private final static Font DAY_FONT = new Font();
        /** Text color to use for the hours of the day. */
        private final static RGB HOUR_TEXT_COLOR = DAY_TEXT_COLOR;
        /** Font Data used to create the font used to draw the hour labels. */
        private final static Font HOUR_FONT = new Font(null, 9, -1);
        /** Text color to use for AM/PM following the hours of the day. */
        private final static RGB AFTERNOON_INDICATOR_COLOR = new RGB(127, 127, 127);
        /** Font Data used to create the font used to draw the AM/PM labels. */
        private final static Font AFTERNOON_INDICATOR_FONT = HOUR_FONT;
        /** Horizontal spacing between the right edge of the text and the left edge of the calendar. */
        private final static int TIME_CALENDAR_SPACING = 4;
        /** Horizontal spacing between the left edge of the text and the left edge of the control. */
        private final static int TIME_EDGE_INDENT = 10;
        /** Distance between the top edge of the control and the top of the text. */
        private final static int DAY_LABEL_EDGE_SPACING = 3;
        /** Distance between the bottom of the text and the top edge of the calendar. */
        private final static int DAY_LABEL_SPACE_BELOW = 3;
        
        /** Document this calendar is displaying the schedule of. */
        private SchedulerDocument document;
        
        /** Location of the left edge of the calendar (the part where where the hours are shown stops) */
        private int leftEdge;
        /** Height of the day labels. */
        private int dayLabelHeight;
        /** Width of each weekday in the remainder of the calendar */
        private double weekdayWidth;
        /** Distance between each hour in the calendar. */
        private double hourHeight;
        /** Length of day labels to use */
        private Weekday.AbbreviationLevel labelLength;
        /** Left edge of the various right-aligned hour times. (hourEdges[0] (midnight) is unused, since that's not drawn.) */
        private int[] hourLabelLeftEdges = new int[24];
        /** Vertical offset of the top of the hour label, relative to the hour line. */
        private int hourLabelVerticalOffset;
        /** Vertical offset of the top of the am/pm part of the hour label, relative to the hour line. */
        private int afternoonIndicatorVerticalOffset;
        /** Set when the control is resized so that we will recalculate position information the next time we're drawn. */
        private boolean positionInformationChanged;
        
        /** List of events displayed on the calendar. */
        private List<EventDisplayer> events = new ArrayList();
        
        /** List of events that requested to be relayed out. */
        private Set<EventDisplayer> eventsToRelayout = new HashSet();
        
        /** Whether or not we've already scheduled an event to layout. */
        private boolean shouldLayoutEvents = false;
        
        /** Sends notifications to MenuAvailabilityListeners for us. */
        private ListenerManager<MenuAvailabilityListener> listenerManager = new ListenerManager<MenuAvailabilityListener>(new ListenerManager.Notifier()
        {
            public void notifyListener(Object object)
            {
                ((MenuAvailabilityListener)object).availabilityChanged();
            }
        });
        
        /** Listens to changes in documents selection, and forwards that 
         * something has changed to our availability listeners. */
        private KeyValueObserver selectionListener = new KeyValueAdapter()
        {
            public void valueChanged(KeyValueCoding object, String key)
            {
                listenerManager.notifyListeners();
            }
        };
        
        /**
         * Creates a new instance of CalendarInternal
         */
        public CalendarInternal(Composite parent, SchedulerDocument document)
        {
            super(parent, SWT.NO_BACKGROUND);
            this.document = document;
            this.addControlListener(new ControlAdapter()
            {
                public void controlResized(ControlEvent e)
                {
                    positionInformationChanged = true;
                }
            });
            recreateEvents();
            layoutEvents();
            document.getSchedule().addListener(new KeyValueAdapter()
            {
                public void valueChanged(KeyValueCoding object, String key, SetMutationType mutation, Set modifyingSet)
                {
                    // this is also rather inefficient, but it works.
                    recreateEvents();
                    layoutEvents();
                }
            }, Schedule.COURSES_KEY);
            
            document.getSelectionController().addListener(selectionListener, SelectionController.SELECTION_KEY);
        }
        
        /**
         * Clears the list of events and recreates all events from scratch by
         * referencing the document.
         */
        private void recreateEvents()
        {
            // remove all existing events.
            // Use a copy of the original to avoid a concurrent modification to the
            // array as we iterate through it.
            for(EventDisplayer oldDisplayer : new ArrayList<EventDisplayer>(events))
                remove(oldDisplayer);
            
            Set courses = document.getSchedule().getCourses();
            Iterator iterator = courses.iterator();
            while(iterator.hasNext())
            {
                Course thisCourse = (Course) iterator.next();
                createEventsForCoursePart(thisCourse);
            }
        }
        
        /**
         * Creates an event for a course and adds it to the calendar.
         * @param course The course to create an event for.
         */
        private void createEventsForCoursePart(CoursePart coursePart)
        {
            Collection sections = coursePart.getChildren();
            Iterator iterator = sections.iterator();
            while(iterator.hasNext())
            {
                Section section = (Section) iterator.next();
                
                // create the event for this section.
                createEventForSection(section);
                
                // create events for children of this section.
                createEventsForCoursePart(section);
            }
        }
        
        /**
         * Creates an event for a section and adds it to the calendar.
         * @param section The section to create the event for.
         */
        private void createEventForSection(Section section)
        {
            CoursePart selectedPart = document.getSelectionController().getSelection();
            Set meetingTimes = section.getMeetingTimes();
            Iterator iterator = meetingTimes.iterator();
            while(iterator.hasNext())
            {
                MeetingTime meetingTime = (MeetingTime) iterator.next();
                for(Weekday day : meetingTime.getMeetingDays())
                {
                    EventDisplayer displayer = new EventDisplayer(this, meetingTime);
                    displayer.setDayOfWeek(day);
                    displayer.setSelected(section == selectedPart);
                }
            }
        }
        
        /**
         * Sets the selection to the specified course part.
         */
        public void setSelection(CoursePart coursePart)
        {
            document.getSelectionController().setSelection(coursePart);
        }
        
        /** Mouse down event that wasn't handled anywhere else.
         * Clear the current selection.
         * @param e The event to handle.
         */
        public void mouseDown(MouseEvent e)
        {
            setSelection(null);
        }
        
        /**
         * Redoes the layout of all events.
         */
        public void layoutEvents()
        {
            sortEvents(events);
            layoutEvents(events);
        }
        
        /**
         * Redoes the layout of some subset of the events.
         * @param events The events to layout.  They must be sorted by start
         * time.
         */
        public void layoutEvents(List<EventDisplayer> events)
        {
            // reset the events to layout to their default layout parameters.
            for(EventDisplayer thisEvent : events)
            {
                thisEvent.setConflictLevel(1);
                thisEvent.setConflictIndent(0);
                thisEvent.setConflictWidth(1);
            }
            
            // events get added to this list as they are laid out
            List<EventDisplayer> laidOutEvents = new ArrayList(events.size());
            
            // layout all events, one at a time
            for(EventDisplayer thisEvent : events)
                layoutEvent(thisEvent, laidOutEvents);
            
            // events are laid out, but may not occupy as much space as we'd
            // like  Let's try to grow them now.
            growEvents(laidOutEvents);
            
            // Tell the event displayer to recalculate it's drawing layout
            // information
            for(EventDisplayer thisEvent : events)
                thisEvent.redoLayout();
        }
        
        /**
         * Positions an event based on its conflicts with the other events in
         * the array layedOutEvents.  Then, it adds the event to the list of
         * events that have been layed out.
         * This method may change the positioning of other events in the list
         * of already laid out events in order to get this one to fit.
         * @param eventToPosition The event to get to fit.
         * @param laidOutEvents The events it must fit around.
         */
        private void layoutEvent(EventDisplayer eventToPosition, List<EventDisplayer> laidOutEvents)
        {
            
            List<EventDisplayer> conflicts = getConflictingEvents(eventToPosition, laidOutEvents);
            int greatestConflictLevel = 1;
            for(EventDisplayer conflictingEvent : conflicts)
            {
                if (conflictingEvent.getConflictLevel() > greatestConflictLevel)
                    greatestConflictLevel = conflictingEvent.getConflictLevel();
            }
            eventToPosition.setConflictLevel(greatestConflictLevel);
            
            // try to find a place to position this event
            boolean placedEvent = false;
            for(int i = 0; i < greatestConflictLevel; i++)
            {
                placedEvent = true;
                eventToPosition.setConflictIndent(i);
                if (eventOverlapsWithOtherEvents(eventToPosition, conflicts))
                    placedEvent = false;
                if (placedEvent)
                    break;
            }
            
            if (!placedEvent)
            {
                eventToPosition.setConflictIndent(greatestConflictLevel);
                greatestConflictLevel++;
                eventToPosition.setConflictLevel(greatestConflictLevel);
                for(EventDisplayer conflictingEvent : conflicts)
                    conflictingEvent.setConflictLevel(greatestConflictLevel);
            }
            laidOutEvents.add(eventToPosition);
        }
        
        /**
         * Makes events take up as much space as is available.
         * @param eventsToGrow The list of events to grow.  Events grown are
         * only checked against other events in this list, so make sure there
         * are no conflicts outside the list.
         */
        private void growEvents(List<EventDisplayer> eventsToGrow)
        {
            boolean changed;
            do
            {
                changed = false;
                // try growing to the left and to the right
                for(int i = 0; i < 2; i++)
                {
                    boolean growRight = (i == 0);
                    // go through the list and see if we can grow any of the items
                    for(EventDisplayer thisEvent : eventsToGrow)
                    {
                        List<EventDisplayer> conflicts = getConflictingEvents(thisEvent, eventsToGrow);
                        int width = thisEvent.getConflictWidth();
                        int indent = thisEvent.getConflictIndent();
                        int level = thisEvent.getConflictLevel();
                        if ((!growRight && indent > 0) ||
                                (growRight && width+indent < level))
                        {
                            thisEvent.setConflictWidth(width+1);
                            if (!growRight)
                                thisEvent.setConflictIndent(indent-1);
                            if (eventOverlapsWithOtherEvents(thisEvent, conflicts))
                            {
                                thisEvent.setConflictWidth(width);
                                if (!growRight)
                                    thisEvent.setConflictIndent(indent);
                            }
                            else
                            {
                                changed = true;
                            }
                        }
                    }
                }
            } while (changed);
        }
        
        /**
         * Causes layout to (eventually) be done for this event and all its
         * conflicts.
         */
        public void redoLayoutAroundEvent(EventDisplayer event)
        {
            eventsToRelayout.add(event);
            if (!shouldLayoutEvents)
            {
                shouldLayoutEvents = true;
                // insert a runnable into the event queue to do the layout after
                // things stop changing due to whatever triggered this event to
                // appear/disappear.
                DocumentController.getSharedDocumentController().getDisplay().asyncExec(new Runnable()
                {
                    public void run()
                    {
                        shouldLayoutEvents = false;
                        // get a list of all VISIBLE conflicts for the current events.
                        HashSet<EventDisplayer> allAffectedEvents = new HashSet();
                        
                        // that includes newly-visible events.
                        for(EventDisplayer displayer : eventsToRelayout)
                        {
                            if (!displayer.isHidden())
                                allAffectedEvents.add(displayer);
                        }
                        
                        Collection<EventDisplayer> newConflicts = new ArrayList();
                        
                        // repeatedly add conflicts and conflicts' conflicts until
                        // we've got everything added.
                        do
                        {
                            for(EventDisplayer displayer : eventsToRelayout)
                            {
                                List<EventDisplayer> conflicts = getConflictingEvents(displayer, events);
                                for(EventDisplayer conflict : conflicts)
                                {
                                    if (allAffectedEvents.add(conflict))
                                        newConflicts.add(conflict);
                                }
                            }
                            eventsToRelayout.clear();
                            eventsToRelayout.addAll(newConflicts);
                            newConflicts.clear();
                        } while (!eventsToRelayout.isEmpty());
                        
                        // turn the collection of conflicts we found into a sorted list
                        List<EventDisplayer> sortedList = new ArrayList(allAffectedEvents);
                        sortEvents(sortedList);
                        
                        // layout just these events.
                        layoutEvents(sortedList);
                        
                    }
                });
            }
        }
        
        /**
         * Adds the event displayer to the control.  Called in the constructor
         * for EventDisplayer.
         * @param newDisplayer the displayer to add.
         */
        public void add(EventDisplayer newDisplayer)
        {
            events.add(newDisplayer);
            super.add(newDisplayer);
        }
        
        /**
         * Removes the event displayer from the control.  Called in EventDisplayer's
         * remove method.
         * @param displayer The displayer being removed.
         */
        public void remove(EventDisplayer displayer)
        {
            events.remove(displayer);
            MeetingTime meetingTime = displayer.getMeetingTime();
            super.remove(displayer);
        }
        
        /**
         * Test to see whether or not the specified event overlaps with this one when displayed.
         * @return True if they overlap, false if they do not. (given their indents)
         */
        private boolean eventsOverlap(EventDisplayer first, EventDisplayer second)
        {
            // conflict levels are assumed to be identical
            int firstIndent = first.getConflictIndent();
            int firstWidth = first.getConflictWidth();
            int secondIndent = second.getConflictIndent();
            int secondWidth = second.getConflictWidth();
            return !(firstIndent+firstWidth <= secondIndent ||
                    secondIndent+secondWidth <= firstIndent);
        }
        
        /**
         * Check if an event overlaps with its set of possible conflicts.
         * @param event The event to check for overlaps
         * @param conflicts A list of events that conflict with event, and so
         * could potentially overlap.
         * @return True if event overlaps with any of the conflicts, false if
         * it does not.
         */
        private boolean eventOverlapsWithOtherEvents(EventDisplayer event, List<EventDisplayer> conflicts)
        {
            for(EventDisplayer conflictingEvent : conflicts)
            {
                if (eventsOverlap(event, conflictingEvent))
                    return true;
            }
            return false;
        }
        
        /**
         * Sorts a list of events in start time-increasing order.
         * @param events The list to sort.
         */
        private void sortEvents(List<EventDisplayer> events)
        {
            Collections.sort(events, new Comparator<EventDisplayer>()
            {
                public int compare(EventDisplayer first, EventDisplayer second)
                {
                    // first < second means result is negative
                    int dayDifference = first.getDayOfWeek().getDayOfWeek() - second.getDayOfWeek().getDayOfWeek();
                    if (dayDifference != 0)
                        return dayDifference;
                    else
                    {
                        double timeDifference = first.getStartTime() - second.getStartTime();
                        if (timeDifference < 0)
                            return -1;
                        else if (timeDifference > 0)
                            return 1;
                        else
                            return 0;
                    }
                }
            });
        };
        
        /**
         * Get information about other events on the calendar.  Used when
         * positioning events.  This only finds conflicts with other
         * EventDisplayers that are not hidden.
         * @param eventDisplayer the displayer to search for conflicts with. It
         * can be hidden.
         * @return a list of events that conflict with the specified one, sorted
         * in starting-time order.
         */
        public List<EventDisplayer> getConflictingEvents(EventDisplayer eventDisplayer, List<EventDisplayer> events)
        {
            List<EventDisplayer> conflictingEvents = new ArrayList();
            Weekday thisWeekday = eventDisplayer.getDayOfWeek();
            double begin = eventDisplayer.getStartTime();
            double end = begin + eventDisplayer.getDuration();
            for(EventDisplayer otherEvent : events)
            {
                if (otherEvent != eventDisplayer && !otherEvent.isHidden())
                {
                    if (thisWeekday == otherEvent.getDayOfWeek())
                    {
                        double otherBegin = otherEvent.getStartTime();
                        double otherEnd = otherBegin + otherEvent.getDuration();
                        // This logic is duplicated in TimeConflict.java!
                        if ((otherBegin >= begin && otherBegin <= end) ||
                                (otherEnd   >= begin && otherEnd   <= end) ||
                                (begin >= otherBegin && end <= otherEnd))
                        {
                            conflictingEvents.add(otherEvent);
                        }
                    }
                }
            }
            return conflictingEvents;
        }
        
        /**
         * Draws the control.
         * @param e The paint event triggering the drawing.
         */
        public void paintControl(PaintEvent e)
        {
            Point size = getSize();
            GC g = e.gc;
            
            if (positionInformationChanged)
                calculatePositionInformation();
            
            // Clear the background
            g.setBackground(new Color(this.getDisplay(), CALENDAR_BACKGROUND_COLOR));
            g.fillRectangle(0, 0, size.x, size.y);
            
            // Draw night-time colors from midnight to 6 am
            g.setBackground(new Color(this.getDisplay(), NIGHT_COLOR));
            g.fillRectangle(leftEdge, 0, size.x-leftEdge, (int)(6*hourHeight));
            // ...and from 6pm to midnight
            g.fillRectangle(leftEdge, (int)(18*hourHeight), size.x-leftEdge, size.y-(int)(18*hourHeight));
            
            // Draw the horizontal half hour lines.
            g.setForeground(new Color(this.getDisplay(), HALF_HOUR_SEPARATORS));
            for(int i = 0; i < 24; i++)
            {
                int lineY = (int)(hourHeight * (i+.5));
                g.drawLine(leftEdge, lineY, size.x, lineY);
            }
            
            // Draw the horizontal hour lines.
            g.setForeground(new Color(this.getDisplay(), HOUR_SEPARATORS_COLOR));
            for(int i = 1; i < 24; i++)
            {
                int lineY = (int)(hourHeight * i);
                g.drawLine(leftEdge,lineY, size.x, lineY);
            }
            
            // Draw the vertical day separator lines.
            g.setForeground(new Color(this.getDisplay(), DAY_SEPARATORS_COLOR));
            int daysPerWeek = getNumberOfDaysInWeek();
            for(int i = 0; i < daysPerWeek+1; i++)
            {
                int lineX = leftEdge + (int)(i * weekdayWidth);
                g.drawLine(lineX, 0, lineX, size.y);
            }
            
            // Draw the hour text on the left edge of the calendar.
            for(int hour = 1; hour < 24; hour++)
            {
                String hourText = getHourText(hour);
                String ampmText = getAfternoonIndicatorText(hour);
                HOUR_FONT.setFont(g);
                g.setForeground(new Color(this.getDisplay(), HOUR_TEXT_COLOR));
                int ampmLeft = hourLabelLeftEdges[hour] + g.textExtent(hourText).x;
                g.drawText(hourText, hourLabelLeftEdges[hour], hourLabelVerticalOffset+(int)(hour*hourHeight), true);
                AFTERNOON_INDICATOR_FONT.setFont(g);
                g.setForeground(new Color(this.getDisplay(), AFTERNOON_INDICATOR_COLOR));
                g.drawText(ampmText, ampmLeft, afternoonIndicatorVerticalOffset+(int)(hour*hourHeight), true);
            }
        }
        
        /**
         * Draws the day label headers.
         * @param e The paint event to respond to.
         */
        public void paintDayLabels(PaintEvent e)
        {
            if (positionInformationChanged)
                calculatePositionInformation();
            GC g = e.gc;
            
            // clear the background area with the background color.
            g.setBackground(new Color(this.getDisplay(), CALENDAR_BACKGROUND_COLOR));
            g.fillRectangle(e.x, 0, e.width, dayLabelHeight);
            
            // Draw the vertical day separator lines.
            g.setForeground(new Color(this.getDisplay(), DAY_SEPARATORS_COLOR));
            int daysPerWeek = getNumberOfDaysInWeek();
            for(int i = 0; i < daysPerWeek+1; i++)
            {
                int lineX = leftEdge + (int)(i * weekdayWidth);
                g.drawLine(lineX, 0, lineX, dayLabelHeight);
            }
            
            // Draw a horizontal line under separating this part from the calender.
            g.drawLine(e.x, dayLabelHeight-1, e.width, dayLabelHeight-1);
            
            // Draw the text inside the weekday labels
            DAY_FONT.setFont(g);
            g.setForeground(new Color(this.getDisplay(), DAY_TEXT_COLOR));
            Weekday thisDay = getFirstDayOfWeek();
            for(int i =  0; i < daysPerWeek; i++)
            {
                String stringToDraw = labelLength.getLabel(thisDay);
                int textWidth = g.stringExtent(stringToDraw).x;
                int dayLabelLeft = leftEdge + (int)(weekdayWidth * (i+.5) - (textWidth/2.0));
                g.drawText(stringToDraw, dayLabelLeft, DAY_LABEL_EDGE_SPACING, true);
                thisDay = thisDay.getNextDay();
            }
            
        }
        
        /**
         * Recalculates the position of everything on the calendar.  Called when
         * the size changes.
         */
        private void calculatePositionInformation()
        {
            Point size = getSize();
            // we need a GC to calculate string sizes.
            GC g = new GC(this);
            
            int maxWidth = 0;
            for(int i = 1; i < 24; i++)
            {
                HOUR_FONT.setFont(g);
                Point hourSize = g.stringExtent(getHourText(i));
                AFTERNOON_INDICATOR_FONT.setFont(g);
                Point AMPMSize = g.stringExtent(getAfternoonIndicatorText(i));
                int totalWidth = hourSize.x+AMPMSize.x;
                // this isn't actually the left edge, it's the width.  We store the
                // width and subtract it from the left edge of the calendar to get
                // the correct left edge later.
                hourLabelLeftEdges[i] = totalWidth;
                if (totalWidth > maxWidth)
                    maxWidth = totalWidth;
            }
            leftEdge = maxWidth + TIME_CALENDAR_SPACING + TIME_EDGE_INDENT;
            
            // calculate actual hour label text left edges
            for(int i = 1; i < 24; i++)
                hourLabelLeftEdges[i] = leftEdge-TIME_CALENDAR_SPACING-hourLabelLeftEdges[i];
            
            // Figure out the vertical offsets.  Take care to align the text baselines, even if the
            // text is different sizes.
            {
                HOUR_FONT.setFont(g);
                FontMetrics metrics = g.getFontMetrics();
                int hourHeight = metrics.getHeight();
                int hourBaseline = metrics.getDescent();
                AFTERNOON_INDICATOR_FONT.setFont(g);
                metrics = g.getFontMetrics();
                int ampmHeight = metrics.getHeight();
                int ampmBaseline = metrics.getDescent();
                int totalHeight = Math.max(hourHeight, ampmHeight) + Math.abs(ampmBaseline-hourBaseline);
                int relativeOffset = (hourHeight-hourBaseline)-(ampmHeight-ampmBaseline);
                if (relativeOffset > 0)
                {
                    hourLabelVerticalOffset = -totalHeight / 2;
                    afternoonIndicatorVerticalOffset = -(totalHeight-relativeOffset) / 2;
                }
                else
                {
                    hourLabelVerticalOffset = -(totalHeight+relativeOffset) / 2;
                    afternoonIndicatorVerticalOffset = -totalHeight / 2;
                }
            }
                
                // get the distance between the vertical day separators
                weekdayWidth = (size.x-leftEdge) / (double)(getNumberOfDaysInWeek());
                
                // figure out what length labels to use.
                DAY_FONT.setFont(g);
                int daysInWeek = getNumberOfDaysInWeek();
                if (daysInWeek > 7)
                    daysInWeek = 7;
                for(Weekday.AbbreviationLevel abbreviationLevel : Weekday.AbbreviationLevel.values())
                {
                    boolean allFit = true;
                    Weekday thisDay = getFirstDayOfWeek();
                    for(int j = 0; j < daysInWeek; j++)
                    {
                        String label = abbreviationLevel.getLabel(thisDay);
                        int labelSize = g.stringExtent(label).x;
                        if (labelSize > weekdayWidth)
                        {
                            allFit = false;
                            break;
                        }
                        thisDay = thisDay.getNextDay();
                    }
                    labelLength = abbreviationLevel;
                    if (allFit)
                        break;
                }
                
                // figure out the height of the day labels
                FontMetrics metrics = g.getFontMetrics();
                dayLabelHeight = metrics.getHeight() + DAY_LABEL_EDGE_SPACING + DAY_LABEL_SPACE_BELOW;
                
                hourHeight = (size.y / 24.0);
                
                // must dispose of unused Graphics Contexts that we created.
                g.dispose();
                
                positionInformationChanged = false;
        }
        
        /**
         * Get the height information for the labels.
         * @return How much space is required to draw the day labels.
         */
        public int getDayLabelHeight()
        {
            if (dayLabelHeight == 0)
                calculatePositionInformation();
            return dayLabelHeight;
        }
        
        /**
         * Get the height of hours in the control.
         * @return The number of pixels the average hour in the control is.
         * This may not be a whole number.
         */
        public double getHourHeight()
        {
            if (positionInformationChanged)
                calculatePositionInformation();
            return hourHeight;
        }
        
        /**
         * Get the location of the start of a weekday in the control.
         * @param weekday The day of the week you are interested in.
         * @return The offset from the left edge of the control where the
         * specified weekday starts.
         */
        public int getWeekdayStart(Weekday weekday)
        {
            if (positionInformationChanged)
                calculatePositionInformation();
            Weekday weekstart = getFirstDayOfWeek();
            int days = weekday.getDayOfWeek() - weekstart.getDayOfWeek();
            if (days < 0)
                days += 7;
            return (int)(weekdayWidth*days)+leftEdge+1;
        }
        
        /**
         * Get the location of the right edge of a weekday in the control.
         * @param weekday The day of the week you are interested in.
         * @return The offset from the left edge of the control where the
         * specified weekday ends.
         */
        public int getWeekdayEnd(Weekday weekday)
        {
            if (positionInformationChanged)
                calculatePositionInformation();
            Weekday weekstart = getFirstDayOfWeek();
            int days = weekday.getDayOfWeek() - weekstart.getDayOfWeek();
            if (days < 0)
                days += 7;
            return (int)(weekdayWidth*(days+1))+leftEdge-1;
        }
        
        /**
         * How many weekdays are displayed on the calendar?
         * @return The number of days in a week that are being displayed on the
         * calendar.
         */
        public int getNumberOfDaysInWeek()
        {
            return 5;
        }
        
        /**
         * Get the day that the week starts on.
         * @return The first day of the week to be displayed on the calendar.
         */
        public Weekday getFirstDayOfWeek()
        {
            return Weekday.MONDAY;
        }
        
        /**
         * Get the text that should be displayed for a given hour of the day.
         * @param hour The hour of the day.  0 is midnight, 12 is noon, 23 is 11pm.
         * @return The text to be displayed.
         */
        public String getHourText(int hour)
        {
            if (hour == 0)
                return "Midnight";
            else if (hour < 12)
                return hour + " ";
            else if (hour > 12)
                return (hour-12) + " ";
            else
                return "Noon";
        }
        
        /**
         * Get the "AM" or "PM" text that should be displayed for a given hour of
         * the day.
         * @param hour The hour of the day.  0 is midnight, 12 is noon, 23 is 11pm.
         * @return The text to be displayed. (may be an empty string)
         */
        public String getAfternoonIndicatorText(int hour)
        {
            if (hour < 12)
                return "AM";
            else if (hour > 12)
                return "PM";
            return "";
        }
        
        /**
         * Implemented for use by the menu.  Called by the delete menu item when
         * it detects we are selected.
         */
        public void delete()
        {
            CoursePart selected = document.getSelectionController().getSelection();
            if (selected != null)
            {
                document.getSchedule().removeCourse(selected.getCourse());
                document.getUndoManager().setActionName("Delete Course");
            }
        }
        
        /**
         * Used by the menu to monitor the availability of menu items.  Currently
         * only delete is supported, so we ignore the action name parameter.
         * @param actionName The name of the action (which is the same as the 
         * method name) that the menu is checking the availability of.
         * @param listener The listener to add to be notified when the 
         * availability of that menu item changes.
         */
        public void addAvailableListener(String actionName, MenuAvailabilityListener listener)
        {
            listenerManager.addListener(listener);
        }
        
        /**
         * Used by the menu to remove listeners on the availability of menu 
         * items.  Currently only delete is supported, so we ignore the action 
         * name parameter.
         * @param actionName The name of the action (which is the same as the 
         * method name) that the menu is checking the availability of.
         * @param listener The listener to remove from being notified when the 
         * availability of that menu item changes.
         */
        public void removeAvailableListener(String actionName, MenuAvailabilityListener listener)
        {
            listenerManager.removeListener(listener);
        }
        
        /**
         * Used by the menu to check whether a given action is allowed, given
         * the object's current state.  Currently
         * only delete is supported, so we ignore the action name parameter.
         * @param actionName The name of the action (which is the same as the 
         * method name) that the menu is checking the availability of.
         * @return True if the action can be performed now, false if it cannot 
         * be.
         */
        public boolean actionAvailable(String actionName)
        {
            CoursePart selected = document.getSelectionController().getSelection();
            return (selected != null);
        }
    }
    
    /**
     * Displays things on the calendar.
     * Not an instance of a Widget since SWT doesn't seem to support transparent
     * widgets.
     * @author grant
     */
    private static class EventDisplayer extends FakeWidget
    {
        public final static String DEPARTMENT_KEY = "department";
        public final static String COURSE_NUMBER_KEY = "courseNumber";
        public final static String COURSE_NAME_KEY = "courseName";
        public final static String SELECTED_KEY = "selected";
        public final static String CHOSEN_KEY = "chosen";
        public final static String COLOR_KEY = "color";
        public final static String START_TIME_KEY = "startTime";
        public final static String END_TIME_KEY = "endTime";
        
        /** Font used to draw the department. */
        private final static Font DEPARTMENT_FONT = new Font(null, -1, SWT.BOLD);
        /** Font used to draw the course number. */
        private final static Font COURSE_NUMBER_FONT = DEPARTMENT_FONT;
        /** Font used to draw the time of day. */
        private final static Font COURSE_TIME_FONT = COURSE_NUMBER_FONT;
        /** Font used to draw the course title. */
        private final static Font COURSE_NAME_FONT = new Font(null, 9, SWT.NORMAL);
        /** Text shadow offset. */
        private final static Point TEXT_SHADOW_OFFSET = new Point(1, 1);
        /** Radius of the corners. */
        private final static int CORNER_RADIUS = 5;
        /** Offset of the top right corner of the checkbox from the top right corner of the event. */
        private final static Point CHECKBOX_OFFSET = new Point(2, 2);
        /** Inset margin of text from edges of the control */
        private final static Point TEXT_INSET = new Point(4, 4);
        /** Minimum spacing between text and cehckbox. */
        private final static int CHECKBOX_SPACING = 3;
        /** Minimum height of event block, regardless of actual length */
        private final static int MINIMUM_HEIGHT = 22;
        /** Width of the outline when the box is selected. */
        private final static int OUTLINE_WIDTH = 3;
        
        /** Basic color for the event. */
        private AlphaColor color;
        /** Actual color being used to currently draw the control, given its state. */
        private AlphaColor actualColor = color;
        /** Color of the outline (only drawn when the control is selected. */
        private AlphaColor outlineColor = new AlphaColor(0f,0f,0f,.3f);
        /** Text color for the event. */
        private AlphaColor textColor = AlphaColor.BLACK;
        /** Text's shadow color. */
        private AlphaColor shadowColor = new AlphaColor(255, 255, 255, 64);
        
        /** How many slices the column is divided into due to conflicts. */
        private int conflictLevel = 1;
        /** Number of times to indent to the left due to conflicts. */
        private int conflictIndent = 0;
        /** Number of slices of the column to take after indenting */
        private int conflictWidth = 1;
        
        /** Parent calendar control. */
        private CalendarInternal parent;
        /** Checkbox for the corner of the control. */
        private KeyValueCodingCheckbox checkbox;
        
        /** Hour of the day when the event starts. */
        private double eventStartTime = 16;
        /** Duration of event in hours. */
        private double eventDuration = 1.25;
        /** Department of the course. */
        private String department = "CSCI";
        /** Course number of the course. */
        private String courseNumber = "5115";
        /** Course name. */
        private String courseName = "User Interface Design, Implementation and Evaluation";
        /** A string representation of the event's start time. */
        private String startTime = "4:00 PM";
        /** The day of the week the event is on. */
        private Weekday dayOfWeek = Weekday.MONDAY;
        
        /** Binder for department. */
        private Binder departmentBinder;
        /** Binder for course number. */
        private Binder courseNumberBinder;
        /** Binder for course name. */
        private Binder courseNameBinder;
        /** Binder for start time. */
        private Binder startTimeBinder;
        /** Binder for end time. */
        private Binder endTimeBinder;
        /** Binder for color. */
        private Binder colorBinder;
        /** Binds course chosen-ness to our internal chosen-ness */
        private Binder chosenBinder;
        /** Binds internal chosen-ness to checkbox selection. */
        private Binder checkboxBinder;
        
        /** Is there enough space to show the course number? The course number is the last piece of information to be removed. */
        private boolean showCourseNumber;
        /** Is there enough space to show the department name? */
        private boolean showDepartmentName;
        /** Is there enough space to show the course's start time? */
        private boolean showStartTime;
        /** Is there enough space to display any of the course's name and section type? */
        private boolean showCourseDescription;
        /** This is the course name that will actually be displayed.  It may have newline characters in it for wrapping, and it may be cut off. */
        private String displayCourseName;
        /** Location to draw the course number. */
        private Point courseNumberStart = new Point(0, 0);
        /** Location to draw the department name. */
        private Point departmentNameStart = new Point(0, 0);
        /** Location to draw the start time. */
        private Point startTimeStart = new Point(0, 0);
        /** Location to draw the course name. */
        private Point courseNameStart = new Point(0, 0);
        /** True if the control is selected.  Selection is what is mirrored in the list on the left. */
        private boolean selected;
        /** True if the control is previewed.  It may be a child of the selection, and therefore not actually selected. */
        private boolean previewed = true;
        
        /** Actual meeting time event this event displayer displays. */
        private MeetingTime meeting;
        
        /** Document we're inside of. */
        private SchedulerDocument document;
        
        /** Listens to size changes in the parent. */
        private Listener resizeListener = new Listener()
        {
            public void handleEvent(Event event)
            {
                recalculatePosition();
            }
        };
        
        /** Listens to the checkbox to hear when it gets checked. */
        private KeyValueObserver checkboxListener = new KeyValueAdapter()
        {
            public void valueChanged(KeyValueCoding object, String key)
            {
                parent.setSelection(meeting.getParent());
                if (checkbox.getSelection() == true)
                    document.getUndoManager().setActionName("Check Course");
                else
                    document.getUndoManager().setActionName("Uncheck Course");
            }
        };
        
        /** Listen to the document's selction.  When the selection changes,
         * check to see if we're in it or our visibility is affected. */
        private KeyValueObserver selectionListener = new KeyValueAdapter()
        {
            public void valueChanged(KeyValueCoding object, String key)
            {
                CoursePart selectedPart = document.getSelectionController().getSelection();
                setSelected(selectedPart == meeting.getParent());
                checkVisibility();
            }
        };
        
        /** Creates a new instance of EventDisplayer */
        public EventDisplayer(CalendarInternal parent, MeetingTime event)
        {
            this.meeting = event;
            document = (SchedulerDocument) event.getDocument();
            checkbox = new KeyValueCodingCheckbox(parent);
            this.parent = parent;
            this.parent.addListener(SWT.Resize, resizeListener);
            parent.add(this);
            
            Section section = event.getParent();
            Course course = section.getCourse();
            
            /** Binder for department. */
            departmentBinder = new Binder(course, Course.DEPARTMENT_KEY, this, DEPARTMENT_KEY);
            /** Binder for course number. */
            courseNumberBinder = new Binder(course, Course.COURSE_NUMBER_KEY, this, COURSE_NUMBER_KEY);
            /** Binder for course name. */
            courseNameBinder = new Binder(course, Course.NAME_KEY, this, COURSE_NAME_KEY);
            /** Binder for start time. */
            startTimeBinder = new Binder(meeting, MeetingTime.START_TIME_KEY, this, START_TIME_KEY, new TimeTransformer());
            /** Binder for end time. */
            endTimeBinder = new Binder(meeting, MeetingTime.END_TIME_KEY, this, END_TIME_KEY, new TimeTransformer());
            /** Binder for color. */
            colorBinder = new Binder(course, Course.COLOR_KEY, this, COLOR_KEY);
            /** Binders for checkbox. */
            chosenBinder = new Binder(section, Section.CHOSEN_KEY, this, CHOSEN_KEY);
            checkboxBinder = new Binder(section, Section.CHOSEN_KEY, checkbox, KeyValueCodingCheckbox.SELECTION_KEY);
            checkbox.addListener(checkboxListener, KeyValueCodingCheckbox.SELECTION_KEY);
            
            document.getSelectionController().addListener(selectionListener, SelectionController.SELECTION_KEY);
            checkVisibility();
        }
        
        /**
         * Get the conflict width. This is the number of slices that will be
         * occupied by this event.
         * @return The conflict width.
         * @see #setConflictWidth(int)
         */
        public int getConflictWidth()
        {
            return conflictWidth;
        }
        
        /**
         * Get the conflict indent.  This is the number of column slices to
         * indent on the left before starting to draw this event.
         * @return The conflict indent.
         * @see #setConflictIndent(int)
         */
        public int getConflictIndent()
        {
            return conflictIndent;
        }
        
        /**
         * Set the conflict width.  This is the number of column slices occupied
         * by this event.
         * @param conflictWidth The number of slices to occupy.
         * @see #setConflictLevel(int)
         * @see #getConflictWidth()
         */
        public void setConflictWidth(int conflictWidth)
        {
            this.conflictWidth = conflictWidth;
        }
        
        /**
         * Set the distance to indent this event on the left.
         * @param conflictIndent The number of column slices to indent this
         * conflict on the left.
         * @see #getConflictIndent()
         */
        public void setConflictIndent(int conflictIndent)
        {
            this.conflictIndent = conflictIndent;
        }
        
        /**
         * Get the total conflict level.
         * @return The total number of slices the column is divided into.
         * @see #setConflictLevel(int)
         */
        public int getConflictLevel()
        {
            return conflictLevel;
        }
        
        /**
         * Set the conflict level.  This corresponds to the number of slices
         * the calendar column needs to be divided into to get all events to
         * fit.
         * @param newLevel The new number of divisions that will be necessary.
         */
        public void setConflictLevel(int newLevel)
        {
            this.conflictLevel = newLevel;
        }
        
        /**
         * Get the meeting time this event displayer displays.
         * @return The meeting time for this event displayer
         */
        public MeetingTime getMeetingTime()
        {
            return meeting;
        }
        
        /**
         * Show or hide the control
         * @param b True to hide the control, false to show it.
         */
        public void setHidden(boolean b)
        {
            checkbox.setHidden(b);
            super.setHidden(b);
        }
        
        /**
         * Checks whether or not the event may need to be shown or hidden based
         * on changes to selection or being checked or unchecked.
         */
        private void checkVisibility()
        {
            CoursePart selectedPart = document.getSelectionController().getSelection();
            Section section = meeting.getParent();
            if (section.isChosen() || (section == selectedPart) || section.isChildOf(selectedPart) || section.isAncestorOf(selectedPart))
            {
                // should be visible
                if (isHidden())
                {
                    setHidden(false);
                    // ask the calendar to reposition us and any events
                    // conflicting with us.
                    parent.redoLayoutAroundEvent(this);
                }
            }
            else
            { // should not be visible
                if (!isHidden())
                {
                    setHidden(true);
                    // ask the calendar to reposition us and any events
                    // conflicting with us.
                    parent.redoLayoutAroundEvent(this);
                }
            }
        }
        
        /**
         * Call after changing the conflict level to recalculate layout
         * information.
         */
        public void redoLayout()
        {
            recalculatePosition();
        }
        
        /**
         * Recalculate the position of the event on the calendar (for when the
         * calendar resizes)
         */
        private void recalculatePosition()
        {
            double hourHeight = parent.getHourHeight();
            int y = (int)(hourHeight * eventStartTime);
            int height = (int)(hourHeight * eventDuration);
            if (height < MINIMUM_HEIGHT)
                height = MINIMUM_HEIGHT;
            int x = parent.getWeekdayStart(dayOfWeek);
            int width = parent.getWeekdayEnd(dayOfWeek) - x + 1;
            double conflictSliceWidth = width / (double)(getConflictLevel());
            x += (int)(conflictSliceWidth * getConflictIndent());
            width = (int)(conflictWidth * conflictSliceWidth);
            if (conflictWidth+conflictIndent < conflictLevel)
                width--;
            setBounds(x, y, width, height);
            Point checkboxSize = checkbox.getSize();
            checkbox.setLocation(getX()+width-checkboxSize.x-CHECKBOX_OFFSET.x, getY()+CHECKBOX_OFFSET.y);
            
            // calculate position information for the text.
            // we need a GC to calculate string sizes.
            GC g = new GC(parent);
            DEPARTMENT_FONT.setFont(g);
            Point departmentSize = g.stringExtent(department);
            COURSE_NUMBER_FONT.setFont(g);
            Point courseNumberSize = g.stringExtent(courseNumber);
            COURSE_TIME_FONT.setFont(g);
            Point courseTimeSize = g.stringExtent(startTime);
            Point spaceSize = g.stringExtent(" ");
            
            // width of the main block usable for text.
            int textWidth = width - TEXT_INSET.x*2;
            // width of the top block (next to the checkbox) usable for text
            int titleTextWidth = width - TEXT_INSET.x - checkboxSize.x - CHECKBOX_OFFSET.x - CHECKBOX_SPACING;
            
            showStartTime = true;
            showDepartmentName = true;
            showCourseNumber = true;
            if (departmentSize.x+courseNumberSize.x+courseTimeSize.x+spaceSize.x*2 > titleTextWidth)
            {
                showStartTime = false;
                if (courseNumberSize.x + departmentSize.x + spaceSize.x > titleTextWidth)
                {
                    showDepartmentName = false;
                    if (courseNumberSize.x > titleTextWidth)
                        showCourseNumber = false;
                }
            }
            
            int leftTextPoint = TEXT_INSET.x;
            if (showDepartmentName)
            {
                departmentNameStart.x = leftTextPoint;
                departmentNameStart.y = TEXT_INSET.y;
                leftTextPoint += departmentSize.x + spaceSize.x;
            }
            if (showCourseNumber)
            {
                courseNumberStart.x = leftTextPoint;
                courseNumberStart.y = TEXT_INSET.y;
            }
            
            if (showStartTime)
            {
                // right align next to the check box
                startTimeStart.x = width - CHECKBOX_OFFSET.x - checkboxSize.x - CHECKBOX_SPACING - courseTimeSize.x;
                startTimeStart.y = TEXT_INSET.y;
            }
            
            // figure out how much of the department name we can show, and where to wrap it if necessary.
            courseNameStart.y = TEXT_INSET.y + g.getFontMetrics().getHeight();
            courseNameStart.x = TEXT_INSET.x;
            int textHeight = height - courseNameStart.y - TEXT_INSET.y;
            COURSE_NAME_FONT.setFont(g);
            // \u2014 is an em dash
            displayCourseName = meeting.getParent().getHumanReadableType() + " \u2014 " + courseName;
            displayCourseName = wrapToBox(displayCourseName, textHeight, textWidth, g);
            showCourseDescription = (displayCourseName != null && displayCourseName.length() > 0);
            
            g.dispose();
        }
        
        public boolean mouseUp(MouseEvent e)
        {
            parent.setSelection(meeting.getParent());
            //setSelected(this);
            return true;
        }
        
        /**
         * Produces a wrapped and truncated version of the string.  The string
         * is wrapped using the Graphic context's current font.
         * @param text The string to wrap.  It shouldn't have tabs or returns.
         * @param textHeight The height of the box to wrap the text in.
         * @param textWidth The width of the box to wrap the text in.
         * @param g The GraphicsContext to use to get string size information.
         */
        private String wrapToBox(String text, int textHeight, int textWidth, GC g)
        {
            if (text == null || text.length() < 1)
                return "";
            boolean allFits;
            StringBuffer output = new StringBuffer();
            StringBuffer thisLine = new StringBuffer();
            
            String remainingText = text;
            
            int lineHeight = g.getFontMetrics().getHeight();
            
            int lines = 1;
            while(lines * lineHeight < textHeight && remainingText.length() > 0)
            {
                int blankCharacters = 0;
                boolean wroteLine = false;
                while(blankCharacters < remainingText.length() && remainingText.charAt(blankCharacters) == ' ')
                    blankCharacters++;
                remainingText = remainingText.substring(blankCharacters);
                int i;
                for(i = 0; i < remainingText.length(); i++)
                {
                    thisLine.append(remainingText.charAt(i));
                    if (g.stringExtent(thisLine.toString()).x>textWidth)
                    {
                        int usedCharacters = thisLine.lastIndexOf(" ");
                        if (usedCharacters <= 0)
                        {
                            usedCharacters = i-1;
                            if (usedCharacters < 0)
                                usedCharacters = 0;
                        }
                        if (lines > 1)
                            output.append("\n");
                        output.append(thisLine.substring(0, usedCharacters));
                        thisLine.delete(0, thisLine.length());
                        lines++;
                        remainingText = remainingText.substring(usedCharacters+1);
                        wroteLine = true;
                        break;
                    }
                }
                if (!wroteLine)
                {
                    if (lines > 1)
                        output.append("\n");
                    output.append(thisLine);
                    remainingText = "";
                }
            }
            
            return output.toString();
        }
        
        /**
         * Paint the event directly onto the calendar.
         * @param g The graphics context to use.  It should be configured so
         * that the upper left corner of the control corresponds to 0,0.
         */
        public void paintComponent(GC g)
        {
            actualColor.setBackground(g);
            g.fillRoundRectangle(0, 0, getWidth(), getHeight(), CORNER_RADIUS*2, CORNER_RADIUS*2);
            if (selected)
            {
                outlineColor.setForeground(g);
                g.setLineWidth(OUTLINE_WIDTH);
                int inset = OUTLINE_WIDTH/2;
                g.drawRoundRectangle(inset,inset, getWidth()-OUTLINE_WIDTH, getHeight()-OUTLINE_WIDTH, CORNER_RADIUS*2, CORNER_RADIUS*2);
            }
            
            if (showDepartmentName)
                drawText(g, department, departmentNameStart, DEPARTMENT_FONT, textColor, shadowColor);
            if (showCourseNumber)
                drawText(g, courseNumber, courseNumberStart, COURSE_NUMBER_FONT, textColor, shadowColor);
            if (showStartTime)
                drawText(g, startTime, startTimeStart, COURSE_TIME_FONT, textColor, shadowColor);
            if (showCourseDescription)
                drawText(g, displayCourseName, courseNameStart, COURSE_NAME_FONT, textColor, shadowColor);
        }
        
        /**
         * Draw some text on the control.
         * @param g Graphics context to use to draw.
         * @param text String to draw.  g.drawText is used, so returns and tabs
         * are processed.
         * @param location The point to draw the top-left corner of the text at.
         * @param font The font to use.
         * @param color The color to use.
         */
        private void drawText(GC g, String text, Point location, Font font, AlphaColor color, AlphaColor shadowColor)
        {
            font.setFont(g);
            if (shadowColor != null)
            {
                shadowColor.setForeground(g);
                g.drawText(text, location.x+TEXT_SHADOW_OFFSET.x, location.y+TEXT_SHADOW_OFFSET.y, true);
            }
            color.setForeground(g);
            g.drawText(text, location.x, location.y, true);
        }
        
        /**
         * Remove the control.
         */
        public void removeFromParent()
        {
            // remove bindings (otherwise, we would be retained for these notifications
            // even after being removed from display.
            departmentBinder.unbind();
            courseNumberBinder.unbind();
            courseNameBinder.unbind();
            startTimeBinder.unbind();
            endTimeBinder.unbind();
            colorBinder.unbind();
            chosenBinder.unbind();
            checkboxBinder.unbind();
            
            parent.removeListener(SWT.Resize, resizeListener);
            document.getSelectionController().removeListener(selectionListener, SelectionController.SELECTION_KEY);
            checkbox.removeListener(checkboxListener, KeyValueCodingCheckbox.SELECTION_KEY);
            checkbox.dispose();
        }
        
        /**
         * Sets whether or not the control is selected.
         * @param selected True if the control is selected, false if it is not.
         */
        private void setSelected(boolean selected)
        {
            if (this.selected != selected)
            {
                this.selected = selected;
                calculateActualColor();
                redraw();
            }
        }
        
        /**
         * Sets whether or not the control is chosen (not previewed).
         * @param previewed True if the control is chosen, false if it is only previewed.
         */
        public void setChosen(boolean chosen)
        {
            if (this.previewed == previewed)
            {
                this.previewed = !chosen;
                meeting.getParent().setChosen(chosen);
                calculateActualColor();
                redraw();
            }
        }
        
        public void setColor(AlphaColor color)
        {
            if (color != null && !color.equals(this.color))
            {
                this.color = color;
                calculateActualColor();
                redraw();
            }
        }
        
        private void calculateActualColor()
        {
            if (!selected)
                actualColor = color.blendWithColor(.2f, AlphaColor.WHITE);
            else
                actualColor = color;
            if (previewed)
                actualColor = new AlphaColor(actualColor.getRed(), actualColor.getGreen(), actualColor.getBlue(), 128);
        }
        
        public AlphaColor getColor()
        {
            return color;
        }
        
        public double getStartTime()
        {
            return eventStartTime;
        }
        
        public void setStartTime(double eventStartTime)
        {
            if (this.eventStartTime != eventStartTime)
            {
                this.eventStartTime = eventStartTime;
                int displayHour = (int)eventStartTime;
                int displayMinute = (int)Math.round((eventStartTime - displayHour) * 60);
                boolean afternoon = false;
                if (displayHour > 12)
                {
                    displayHour -= 12;
                    afternoon = true;
                }
                else if (displayHour == 12)
                    afternoon = true;
                if (displayHour == 0)
                    displayHour = 12;
                startTime = displayHour+":"+addLeadingZeroes(2, String.valueOf(displayMinute)) + " ";
                if (afternoon)
                    startTime += "PM";
                else
                    startTime += "AM";
                recalculatePosition();
            }
        }
        
        public void setEndTime(double endTime)
        {
            setDuration((endTime - eventStartTime));
        }
        
        public double getEndTime()
        {
            return eventStartTime + eventDuration;
        }
        
        /**
         * Adds zeroes to the start of a string until it is at least a specific length.
         * @param length The total number of characters that should be in the final string.
         * @param value The string value to add zeroes to the front of.
         * @return The string value, preceded by as many zeros as necessary to
         * make it at least length long.
         */
        private String addLeadingZeroes(int length, String value)
        {
            while(value.length() < length)
            {
                value = "0" + value;
            }
            return value;
        }
        
        public double getDuration()
        {
            return eventDuration;
        }
        
        public void setDuration(double eventDuration)
        {
            if (this.eventDuration != eventDuration)
            {
                this.eventDuration = eventDuration;
                recalculatePosition();
            }
        }
        
        public String getDepartment()
        {
            return department;
        }
        
        public void setDepartment(String department)
        {
            this.department = department;
        }
        
        public String getCourseNumber()
        {
            return courseNumber;
        }
        
        public void setCourseNumber(String courseNumber)
        {
            this.courseNumber = courseNumber;
        }
        
        public String getCourseName()
        {
            return courseName;
        }
        
        public void setCourseName(String courseName)
        {
            this.courseName = courseName;
        }
        
        public boolean isSelected()
        {
            return selected;
        }
        
        public boolean isChosen()
        {
            return !previewed;
        }
        
        public Weekday getDayOfWeek()
        {
            return dayOfWeek;
        }
        
        public void setDayOfWeek(Weekday dayOfWeek)
        {
            this.dayOfWeek = dayOfWeek;
        }
        
        private static class TimeTransformer implements ValueTransformer
        {
            /**
             * Transform a Date object into a double representing the time of day.
             */
            public Object transform(Object value)
            {
                Date date = (Date) value;
                GregorianCalendar calendar = new GregorianCalendar();
                calendar.setTime(date);
                int hours = calendar.get(calendar.HOUR_OF_DAY);
                int minutes = calendar.get(calendar.MINUTE);
                return hours + (minutes / 60.0);
            }
            
            /**
             * Transforms a double representing the time of day into a date.
             */
            public Object reverseTransform(Object value)
            {
                double hours = ((Number)value).doubleValue();
                int hourOfDay = (int)hours;
                int minute = (int)Math.round((hours-hourOfDay)*60.0);
                GregorianCalendar calendar = new GregorianCalendar();
                calendar.set(calendar.HOUR_OF_DAY, hourOfDay);
                calendar.set(calendar.MINUTE, minute);
                return calendar.getTime();
            }
            
        }
    }
    
}





